package com.zoe.onelink.redis.util;

import cn.hutool.core.io.resource.ResourceUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.zoe.onelink.common.enumeration.ExceptionCodeEnum;
import com.zoe.onelink.common.exception.ApiException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * <p>标题: </p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2021</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2021-10-15
 */
@RequiredArgsConstructor
public class RedisUtil implements InitializingBean {

    /**
     * RedisUtil 对象
     */
    private static RedisUtil self;

    /**
     * Redis 批量删除脚本对象
     */
    private static RedisScript<Long> MULTI_DEL_SCRIPT;

    /**
     * Redis 操作模板对象
     */
    public final RedisTemplate<String, Object> redisTemplate;

    /**
     * Redis 字符串操作模板对象
     */
    public final StringRedisTemplate stringRedisTemplate;

    static {
        try {
            String scriptStr = ResourceUtil.readUtf8Str("script/redis/param-multi-del.lua");
            MULTI_DEL_SCRIPT = new DefaultRedisScript<>(scriptStr, Long.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        self = this;
    }

    /**
     * 获取 Redis 中的字符串值
     *
     * @param redisKey - Redis key
     * @return String - Redis 中存储的字符串值
     */
    public static String getString(String redisKey) {
        Object retVal = getRedisTemplate().opsForValue().get(redisKey);
        if (retVal == null) {
            return null;
        } else if (retVal instanceof JSONObject) {
            return ((JSONObject) retVal).toJSONString();
        } else if (retVal instanceof String) {
            return (String) retVal;
        }
        return JSON.toJSONString(retVal);
    }

    /**
     * 获取 Redis 中的对象值
     *
     * @param redisKey - Redis key
     * @return Object - Redis 中存储的对象值
     */
    public static Object getObject(String redisKey) {
        return getRedisTemplate().opsForValue().get(redisKey);
    }

    /**
     * 获取 Redis 中的对象值
     *
     * @param redisKey - Redis key
     * @param clazz    - 要转换成的对象类型的 Class 对象
     * @param <T>      - 要转换成的对象类型
     * @return T - Redis 中存储的对象值
     * @throws ApiException - Redis 反序列化失败抛出的异常
     */
    public static <T> T getObject(String redisKey, Class<T> clazz) throws ApiException {
        Object retVal = getRedisTemplate().opsForValue().get(redisKey);
        if (retVal == null) {
            return null;
        }
        T retObj;
        if (clazz.isInstance(retVal)) {
            retObj = (T) retVal;
        } else if (retVal instanceof String) {
            retObj = JSON.parseObject(retVal.toString(), clazz);
        } else if (retVal instanceof JSONObject) {
            JSONObject jsonObj = (JSONObject) retVal;
            jsonObj.remove("@type");
            retObj = jsonObj.toJavaObject(clazz);
        } else if (retVal instanceof Map) {
            retObj = JSON.parseObject(JSON.toJSONString(retVal), clazz);
        } else {
            throw ApiException.createEx(ExceptionCodeEnum.REDIS_DESERIALIZE_FAILURE, redisKey);
        }
        return retObj;
    }

    /**
     * 获取 Redis 中的 List 类型值
     *
     * @param redisKey - Redis key
     * @param clazz    - 要转换成的对象类型的 Class 对象
     * @param <T>      - 要转换成的对象类型
     * @return List<T> - Redis 中存储的 List 类型的值
     */
    public static <T> List<T> getList(String redisKey, Class<T> clazz) {
        Object retVal = getRedisTemplate().opsForValue().get(redisKey);
        List<T> result;
        if (retVal instanceof JSONArray) {
            result = ((JSONArray) retVal).toJavaList(clazz);
        } else {
            result = (List<T>) retVal;
        }
        return result;
    }

    /**
     * 获取 Redis 中的多个值
     *
     * @param keys - Redis keys 集合
     * @param <T>  - 要转换成的对象类型
     * @return List<T> - Redis 中存储的多个值列表
     */
    public static <T> List<T> getMulti(@NonNull Collection<String> keys) {
        return (List<T>) getRedisTemplate().opsForValue().multiGet(keys);
    }

    /**
     * 获取 Redis 中的多个值
     *
     * @param keyPattern - Redis key 正则表达式
     * @param <T>        - 要转换成的对象类型
     * @return List<T> - Redis 中存储的多个值列表
     */
    public static <T> List<T> getMulti(@NonNull String keyPattern) {
        return getMulti(RedisUtil.keys(keyPattern));
    }

    /**
     * 获取 Redis 中所有的值
     *
     * @param <T> - 要转换成的对象类型
     * @return List<T> - Redis 中存储的所有值列表
     */
    public static <T> List<T> findAll() {
        Set<String> keys = keys("*");
        return getMulti(keys);
    }

    /**
     * 判断 Redis 中是否存在指定的 key
     *
     * @param redisKey - Redis key
     * @return boolean - 是否存在
     */
    public static boolean hasKey(@NonNull String redisKey) {
        return Optional.ofNullable(getRedisTemplate().hasKey(redisKey)).orElse(false);
    }

    /**
     * 获取 Redis 中 key 的过期时间
     *
     * @param redisKey - Redis key
     * @param timeUnit - 过期时间单位
     * @return Long - 过期时间
     */
    public static Long getExpire(@NonNull String redisKey, @NonNull TimeUnit timeUnit) {
        return getRedisTemplate().getExpire(redisKey, timeUnit);
    }

    /**
     * 设置 Redis 中的值
     *
     * @param redisKey - Redis key
     * @param value    - Redis 值
     */
    public static void set(@NonNull String redisKey, Object value) {
        getRedisTemplate().opsForValue().set(redisKey, value);
    }

    /**
     * 设置 Redis 中的值及其过期时间
     *
     * @param redisKey   - Redis key
     * @param value      - Redis 值
     * @param expireTime - 过期时间
     * @param timeUnit   - 过期时间单位
     * @return
     */
    public static boolean set(@NonNull String redisKey, Object value, int expireTime, TimeUnit timeUnit) {
        set(redisKey, value);
        return Optional.ofNullable(getRedisTemplate().expire(redisKey, expireTime, timeUnit)).orElse(false);
    }

    /**
     * 在Redis中设置键的值。
     *
     * @param redisKey   Redis中的键。
     * @param value      要设置的值。
     * @param expireTime 键值对的过期时间。
     * @return 如果成功设置了键值对，则返回true，否则返回false。
     */
    public static boolean set(@NonNull String redisKey, Object value, Date expireTime) {
        set(redisKey, value);
        return Optional.ofNullable(getRedisTemplate().expireAt(redisKey, expireTime)).orElse(false);
    }

    /**
     * 在Redis中设置键的过期时间。
     *
     * @param redisKey 要设置过期时间的Redis键。
     * @param expire   键的过期时间。
     * @param timeUnit 过期时间的时间单位。
     * @return 如果成功设置了过期时间，则返回true，否则返回false。
     */
    public static boolean expire(@NonNull String redisKey, int expire, TimeUnit timeUnit) {
        return Optional.ofNullable(getRedisTemplate().expire(redisKey, expire, timeUnit)).orElse(false);
    }

    /**
     * 仅在Redis中键不存在时设置键的值。
     *
     * @param redisKey Redis中的键。
     * @param value    要设置的值。
     * @return 如果成功设置了键值对，则返回true，否则返回false。
     */
    public static boolean setIfAbsent(@NonNull String redisKey, Object value) {
        return Optional.ofNullable(getRedisTemplate().opsForValue().setIfAbsent(redisKey, value)).orElse(false);
    }

    /**
     * 仅在Redis中键不存在时设置键的值并设置过期时间。
     *
     * @param redisKey Redis中的键。
     * @param value    要设置的值。
     * @param expire   键的过期时间。
     * @param timeUnit 过期时间的时间单位。
     * @return 如果成功设置了键值对，则返回true，否则返回false。
     */
    public static boolean setIfAbsent(@NonNull String redisKey, Object value, long expire, TimeUnit timeUnit) {
        return Optional.ofNullable(getRedisTemplate().opsForValue().setIfAbsent(redisKey, value, expire, timeUnit)).orElse(false);
    }

    /**
     * 仅在Redis中键存在时设置键的值。
     *
     * @param redisKey Redis中的键。
     * @param value    要设置的值。
     * @return 如果成功设置了键值对，则返回true，否则返回false。
     */
    public static boolean setIfPresent(@NonNull String redisKey, Object value) {
        return Optional.ofNullable(getRedisTemplate().opsForValue().setIfPresent(redisKey, value)).orElse(false);
    }

    /**
     * 仅在Redis中键存在时设置键的值并设置过期时间。
     *
     * @param redisKey Redis中的键。
     * @param value    要设置的值。
     * @param expire   键的过期时间。
     * @param timeUnit 过期时间的时间单位。
     * @return 如果成功设置了键值对，则返回true，否则返回false。
     */
    public static boolean setIfPresent(@NonNull String redisKey, Object value, long expire, TimeUnit timeUnit) {
        return Optional.ofNullable(getRedisTemplate().opsForValue().setIfPresent(redisKey, value, expire, timeUnit)).orElse(false);
    }


    /**
     * @return 自增长后的值
     */
    public static long increment(String redisKey, long value) {
        return Optional.ofNullable(getRedisTemplate().opsForValue().increment(redisKey, value)).orElse(-1L);
    }

    /**
     * 删除单个Key
     */
    public static boolean deleteOne(@NonNull String redisKey) {
        return Optional.ofNullable(getRedisTemplate().delete(redisKey)).orElse(false);
    }

    /**
     * 删除多个Key
     *
     * @param keyPattern Key匹配表达式
     */
    public static Long deleteKeys(String keyPattern) {
        if ("*".equals(keyPattern)) {
            throw new IllegalArgumentException("Redis delete key pattern could not be '*'");
        }
        return executeScript(MULTI_DEL_SCRIPT, List.of(keyPattern), Long.class);
    }

    /**
     * @return 返回删除数量 , -1 表示删除失败
     */
    public static Long deleteKeys(@NonNull Collection<String> keys) {
        return Optional.ofNullable(getRedisTemplate().delete(keys)).orElse(-1L);
    }

    /**
     * 获取Redis中哈希表中指定键的值。
     *
     * @param redisKey Redis中的哈希表的键。
     * @param mapKey   哈希表中要获取的值的键。
     * @return 哈希表中指定键的值。
     */
    public static Object getMapValue(@NonNull String redisKey, @NonNull String mapKey) {
        return getRedisTemplate().opsForHash().get(redisKey, mapKey);
    }

    /**
     * 获取Redis中哈希表的所有条目。
     *
     * @param redisKey Redis中的哈希表的键。
     * @return 包含哈希表的所有条目的Map对象。
     */
    public static Map<Object, Object> getMapEntries(@NonNull String redisKey) {
        return getRedisTemplate().opsForHash().entries(redisKey);
    }

    /**
     * 检查Redis中的哈希表是否包含指定的键。
     *
     * @param redisKey Redis中的哈希表的键。
     * @param mapKey   要检查的键。
     * @return 如果哈希表包含指定的键，则返回true，否则返回false。
     */
    public static boolean hasMapKey(@NonNull String redisKey, @NonNull String mapKey) {
        return getRedisTemplate().opsForHash().hasKey(redisKey, mapKey);
    }

    /**
     * 在Redis中哈希表中设置键值对。
     *
     * @param redisKey Redis中的哈希表的键。
     * @param mapKey   要设置的键。
     * @param mapValue 要设置的值。
     */
    public static void putMapValue(@NonNull String redisKey, @NonNull String mapKey, Object mapValue) {
        getRedisTemplate().opsForHash().put(redisKey, mapKey, mapValue);
    }

    /**
     * 在Redis中哈希表中设置多个键值对。
     *
     * @param redisKey Redis中的哈希表的键。
     * @param map      包含要设置的键值对的Map对象。
     */
    public static void putMap(@NonNull String redisKey, Map<String, Object> map) {
        getRedisTemplate().opsForHash().putAll(redisKey, map);
    }

    /**
     * 获取与指定模式匹配的所有键的集合。
     *
     * @param keyPattern 匹配键的模式。
     * @return 与指定模式匹配的所有键的集合。
     */
    public static Set<String> keys(@NonNull String keyPattern) {
        return getRedisTemplate().keys(keyPattern);
    }

    /**
     * 执行Redis脚本
     *
     * @param redisScript 脚本
     * @param returnType  返回类型
     * @param keys        keys
     * @param args        args
     */
    public static <T> T executeScript(String redisScript, Class<T> returnType, List<String> keys, Object...
            args) {
        DefaultRedisScript<T> script = new DefaultRedisScript<>(redisScript, returnType);
        return executeScript(script, keys, returnType, args);
    }

    /**
     * 执行Redis脚本
     *
     * @param redisScript 脚本
     * @param keys        keys
     * @param args        args
     */
    public static <T> T executeScript(RedisScript<T> redisScript, List<String> keys, Class<T> returnType, Object... args) {
        T result = getRedisTemplate().execute(redisScript, keys, args);
        return result instanceof JSONObject ? ((JSONObject) result).toJavaObject(returnType) : result;
    }

    public static RedisTemplate<String, Object> getRedisTemplate() {
        return self.redisTemplate;
    }

    public static StringRedisTemplate getStringRedisTemplate() {
        return self.stringRedisTemplate;
    }
}
